import Callout from 'components/Callout';
import Details from 'components/Details';

# Middleware

<Callout>
  The middleware is only needed when you're using [i18n
  routing](/docs/getting-started/app-router).
</Callout>

The middleware handles redirects and rewrites based on the detected user locale.

```tsx filename="middleware.ts"
import createMiddleware from 'next-intl/middleware';
import {locales} from './config';

export default createMiddleware({
  // A list of all locales that are supported
  locales,

  // Used when no locale matches
  defaultLocale: 'en'
});

export const config = {
  // Match only internationalized pathnames
  matcher: ['/', '/(de|en)/:path*']
};
```

In addition to handling i18n routing, the middleware sets the `link` header to inform search engines that your content is available in different languages (see [alternate links](#alternate-links)).

## Locale detection

The locale is negotiated based on your [`localePrefix`](/docs/routing#locale-prefix) and [`domains`](/docs/routing#domains) setting. Once a locale is detected, it will be remembered for future requests by being stored in the `NEXT_LOCALE` cookie.

### Prefix-based routing (default) [#location-detection-prefix]

By default, [prefix-based routing](/docs/routing#locale-prefix) is used to determine the locale of a request.

In this case, the locale is detected based on these priorities:

1. A locale prefix is present in the pathname (e.g. `/en/about`)
2. A cookie is present that contains a previously detected locale
3. A locale can be matched based on the [`accept-language` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language)
4. As a last resort, the `defaultLocale` is used

To change the locale, users can visit a prefixed route. This will take precedence over a previously matched locale that is saved in a cookie or the `accept-language` header and will update the previous cookie value.

**Example workflow:**

1. A user requests `/` and based on the `accept-language` header, the `en` locale is matched.
2. The `en` locale is saved in a cookie and the user is redirected to `/en`.
3. The app renders `<Link locale="de" href="/">Switch to German</Link>` to allow the user to change the locale to `de`.
4. When the user clicks on the link, a request to `/de` is initiated.
5. The middleware will update the cookie value to `de`.

<Details id="accept-language-matching">
<summary>Which algorithm is used to match the accept-language header against the available locales?</summary>

To determine the best-matching locale based on the available options from your app, the middleware uses the "best fit" algorithm of [`@formatjs/intl-localematcher`](https://www.npmjs.com/package/@formatjs/intl-localematcher). This algorithm is expected to provide better results than the more conservative "lookup" algorithm that is specified in [RFC 4647](https://www.rfc-editor.org/rfc/rfc4647.html#section-3.4).

To illustrate this with an example, let's consider your app supports these locales:

1. `en-US`
2. `de-DE`

The "lookup" algorithm works by progressively removing subtags from the user's `accept-language` header until a match is found. This means that if the user's browser sends the `accept-language` header `en-GB`, the "lookup" algorithm will not find a match, resulting in the default locale being used.

In contrast, the "best fit" algorithm compares the _distance_ between the user's `accept-language` header and the available locales, while taking into consideration regional information. Due to this, the "best fit" algorithm is able to match `en-US` as the best-matching locale in this case.

</Details>

### Domain-based routing [#location-detection-domain]

If you're using [domain-based routing](/docs/routing#domains), the middleware will match the request against the available domains to determine the best-matching locale. To retrieve the domain, the host is read from the `x-forwarded-host` header, with a fallback to `host` (hosting platforms typically provide these headers out-of-the-box).

The locale is detected based on these priorities:

1. A locale prefix is present in the pathname (e.g. `ca.example.com/fr`)
2. A locale is stored in a cookie and is supported on the domain
3. A locale that the domain supports is matched based on the [`accept-language` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language)
4. As a fallback, the `defaultLocale` of the domain is used

Since the middleware is aware of all your domains, if a domain receives a request for a locale that is not supported (e.g. `en.example.com/fr`), it will redirect to an alternative domain that does support the locale.

**Example workflow:**

1. The user requests `us.example.com` and based on the `defaultLocale` of this domain, the `en` locale is matched.
2. The app renders `<Link locale="fr" href="/">Switch to French</Link>` to allow the user to change the locale to `fr`.
3. When the link is clicked, a request to `us.example.com/fr` is initiated.
4. The middleware recognizes that the user wants to switch to another domain and responds with a redirect to `ca.example.com/fr`.

<Details id="domain-matching">
<summary>How is the best matching domain for a given locale detected?</summary>

The bestmatching domain is detected based on these priorities:

1. Stay on the current domain if the locale is supported here
2. Use an alternative domain where the locale is configured as the `defaultLocale`
3. Use an alternative domain where the available `locales` are restricted and the locale is supported
4. Use an alternative domain that supports all locales

</Details>

<Details id="domain-local-testing">
<summary>How can I locally test if my setup is working?</summary>

Since the negotiated locale depends on the host of the request, you can test your setup by attaching a corresponding `x-forwarded-host` header. To achieve this in the browser, you can use a browser extension like [ModHeader in Chrome](https://chromewebstore.google.com/detail/modheader-modify-http-hea/idgpnmonknjnojddfkpgkljpfnnfcklj) and add a setting like:

```
X-Forwarded-Host: example.com
```

With this, your domain config for this particular domain will be used.

</Details>

## Configuration

The middleware accepts a number of configuration options that are [shared](/docs/routing#shared-configuration) with the [navigation APIs](/docs/routing/navigation). This list contains all options that are specific to the middleware.

### Default locale [#default-locale]

The `defaultLocale` is used as a fallback when none of the available locales match the user's request.

```tsx filename="middleware.ts" {6}
import createMiddleware from 'next-intl/middleware';

export default createMiddleware({
  // ... other config

  defaultLocale: 'de'
});
```

### Turning off locale detection [#locale-detection-false]

If you want to rely entirely on the URL to resolve the locale, you can set the `localeDetection` property to `false`. This will disable locale detection based on the `accept-language` header and a potentially existing cookie value from a previous visit.

```tsx filename="middleware.ts" {6}
import createMiddleware from 'next-intl/middleware';

export default createMiddleware({
  // ... other config

  localeDetection: false
});
```

In this case, only the locale prefix and a potentially [matching domain](#domain-based-routing) are used to determine the locale.

Note that by setting this option, the middleware will no longer return a `set-cookie` response header, which can be beneficial for CDN caching (see e.g. [the Cloudflare Cache rules for `set-cookie`](https://developers.cloudflare.com/cache/concepts/cache-behavior/#interaction-of-set-cookie-response-header-with-cache)).

### Alternate links [#alternate-links]

The middleware automatically sets [the `link` header](https://developers.google.com/search/docs/specialty/international/localized-versions#http) to inform search engines that your content is available in different languages. Note that this automatically integrates with your routing strategy and will generate the correct links based on your configuration.

However, there are cases where you may want to provide these links yourself:

1. You have pages that are only available for certain locales
2. You're using an external system like a CMS to manage localized slugs of your pages

In this case, you can opt-out of this behavior by setting `alternateLinks` to `false`.

```tsx filename="middleware.ts" {6}
import createMiddleware from 'next-intl/middleware';

export default createMiddleware({
  // ... other config

  alternateLinks: false // Defaults to `true`
});
```

If you decide to manage alternate links yourself, a good option can be to include them in a [sitemap](/docs/environments/actions-metadata-route-handlers#sitemap).

<Details id="alternate-links-details">
<summary>Which alternate links are included?</summary>

Using the middleware defaults, the `link` header of a response for `/` will look like this:

```
link: <https://example.com/en>; rel="alternate"; hreflang="en",
      <https://example.com/de>; rel="alternate"; hreflang="de",
      <https://example.com/>; rel="alternate"; hreflang="x-default"
```

The [`x-default`](https://developers.google.com/search/docs/specialty/international/localized-versions#xdefault) entry is included to point to a variant that can be used if no other language matches the user's browser setting. This special entry is reserved for language selection & detection, in our case issuing a 307 redirect to the best matching locale.

Note that middleware configuration is automatically incorporated with the following special cases:

1. **`localePrefix: 'always'` (default)**: The `x-default` entry is only included for `/`, not for nested pathnames like `/about`. The reason is that the default [matcher](#matcher-config) doesn't handle unprefixed pathnames apart from `/`, therefore these URLs could be 404s. Note that this only applies to the optional `x-default` entry, locale-specific URLs are always included.
2. **`localePrefix: 'never'`**: Alternate links are entirely turned off since there might not be unique URLs per locale.

Other configuration options like `domains`, `pathnames` and `basePath` are automatically considered.

</Details>

<Details id="alternate-links-customization">
<summary>Can I customize the alternate links?</summary>

If you need to customize the alternate links, you can either turn them off and provide your own implementation, or if you only need to make minor adaptions, you can [compose the middleware](#composing-other-middlewares) and add your custom logic after the middleware has run:

```tsx filename="middleware.ts"
import createMiddleware from 'next-intl/middleware';
import LinkHeader from 'http-link-header';
import {NextRequest} from 'next/server';

const handleI18nRouting = createMiddleware(/* ... */);

export default async function middleware(request: NextRequest) {
  const response = handleI18nRouting(request);

  // Example: Remove the `x-default` entry
  const link = LinkHeader.parse(response.headers.get('link'));
  link.refs = link.refs.filter((entry) => entry.hreflang !== 'x-default');
  response.headers.set('link', link.toString());

  return response;
}
```

</Details>

### Matcher config

The middleware is intended to only run on pages, not on arbitrary files that you serve independently of the user locale (e.g. `/favicon.ico`).

Because of this, the following config is generally recommended:

```tsx filename="middleware.ts"
export const config = {
  // Match only internationalized pathnames
  matcher: ['/', '/(de|en)/:path*']
};
```

This enables:

1. A redirect at `/` to a suitable locale
2. Internationalization of all pathnames starting with a locale (e.g. `/en/about`)

<Details id="matcher-avoid-hardcoding">
<summary>Can I avoid hardcoding the locales in the `matcher` config?</summary>

A [Next.js `matcher`](https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher) needs to be statically analyzable, therefore you can't use variables to generate this value dynamically. However, you can implement the matcher dynamically instead:

```tsx filename="middleware.ts"
import {NextRequest} from 'next/server';
import createMiddleware from 'next-intl/middleware';

const intlMiddleware = createMiddleware({
  // ...
});

export default function middleware(request: NextRequest) {
  const {pathname} = request.nextUrl;

  const shouldHandle =
    pathname === '/' ||
    new RegExp(`^/(${locales.join('|')})(/.*)?$`).test(
      request.nextUrl.pathname
    );
  if (!shouldHandle) return;

  return intlMiddleware(request);
}
```

</Details>

#### Pathnames without a locale prefix [#matcher-no-prefix]

There are two use cases where you might want to match pathnames without a locale prefix:

1. You're using a config for [`localePrefix`](/docs/routing#locale-prefix) other than [`always`](/docs/routing#locale-prefix-always)
2. You want to implement redirects that add a locale for unprefixed pathnames (e.g. `/about` → `/en/about`)

For these cases, the middleware should run on requests for pathnames without a locale prefix as well.

A popular strategy is to match all routes that don't start with certain segments (e.g. `/_next`) and also none that include a dot (`.`) since these typically indicate static files. However, if you have some routes where a dot is expected (e.g. `/users/jane.doe`), you should explicitly provide a matcher for these.

```tsx filename="middleware.ts"
export const config = {
  // Matcher entries are linked with a logical "or", therefore
  // if one of them matches, the middleware will be invoked.
  matcher: [
    // Match all pathnames except for
    // - … if they start with `/api`, `/_next` or `/_vercel`
    // - … the ones containing a dot (e.g. `favicon.ico`)
    '/((?!api|_next|_vercel|.*\\..*).*)',
    // However, match all pathnames within `/users`, optionally with a locale prefix
    '/([\\w-]+)?/users/(.+)'
  ]
};
```

{/* Keep this in sync with `packages/next-intl/test/middleware/middleware.test.tsx` */}

Note that some third-party providers like [Vercel Analytics](https://vercel.com/analytics) and [umami](https://umami.is/docs/running-on-vercel) typically use internal endpoints that are then rewritten to an external URL (e.g. `/_vercel/insights/view`). Make sure to exclude such requests from your middleware matcher so they aren't rewritten by accident.

### Base path

The `next-intl` middleware as well as [the navigation APIs](/docs/routing/navigation) will automatically pick up a [`basePath`](https://nextjs.org/docs/app/api-reference/next-config-js/basePath) that you might have configured in your `next.config.js`.

Note however that you should make sure that your [middleware `matcher`](#matcher-config) handles the root of your base path:

```tsx filename="middleware.ts"
export const config = {
  // The `matcher` is relative to the `basePath`
  matcher: [
    // This entry handles the root of the base
    // path and should always be included
    '/'

    // ... other matcher config
  ]
};
```

### Trailing slash

If you have [`trailingSlash`](https://nextjs.org/docs/app/api-reference/next-config-js/trailingSlash) set to `true` in your Next.js config, this setting will be taken into account when the middleware generates pathnames, e.g. for redirects.

Note that if you're using [localized pathnames](/docs/routing#pathnames), your internal and external pathnames can be defined either with or without a trailing slash as they will be normalized internally.

## Composing other middlewares

By calling `createMiddleware`, you'll receive a function of the following type:

```tsx
middleware(request: NextRequest): NextResponse
```

If you need to incorporate additional behavior, you can either modify the request before the `next-intl` middleware receives it, or modify the response that is returned.

```tsx filename="middleware.ts"
import createMiddleware from 'next-intl/middleware';
import {NextRequest} from 'next/server';
import {locales} from './config';

export default async function middleware(request: NextRequest) {
  // Step 1: Use the incoming request (example)
  const defaultLocale = request.headers.get('x-your-custom-locale') || 'en';

  // Step 2: Create and call the next-intl middleware (example)
  const handleI18nRouting = createMiddleware({
    locales,
    defaultLocale
  });
  const response = handleI18nRouting(request);

  // Step 3: Alter the response (example)
  response.headers.set('x-your-custom-locale', defaultLocale);

  return response;
}

export const config = {
  // Match only internationalized pathnames
  matcher: ['/', '/(de|en)/:path*']
};
```

### Example: Additional rewrites

If you need to handle rewrites apart from the ones provided by `next-intl`, you can adjust the `pathname` of the `request` before invoking the `next-intl` middleware (based on ["A/B Testing with Cookies" by Vercel](https://vercel.com/templates/next.js/cookies)).

This example rewrites requests for `/[locale]/profile` to `/[locale]/profile/new` if a special cookie is set.

```tsx filename="middleware.ts"
import createMiddleware from 'next-intl/middleware';
import {NextRequest} from 'next/server';
import {locales} from './config';

export default async function middleware(request: NextRequest) {
  const [, locale, ...segments] = request.nextUrl.pathname.split('/');

  if (locale != null && segments.join('/') === 'profile') {
    const usesNewProfile =
      (request.cookies.get('NEW_PROFILE')?.value || 'false') === 'true';

    if (usesNewProfile) {
      request.nextUrl.pathname = `/${locale}/profile/new`;
    }
  }

  const handleI18nRouting = createMiddleware({
    locales,
    defaultLocale: 'en'
  });
  const response = handleI18nRouting(request);
  return response;
}

export const config = {
  matcher: ['/', '/(de|en)/:path*']
};
```

Note that if you use a [`localePrefix`](/docs/routing#locale-prefix) other than `always`, you need to adapt the handling appropriately to handle unprefixed pathnames too.

### Example: Integrating with Clerk

[`@clerk/nextjs`](https://clerk.com/docs/references/nextjs/overview) provides a middleware that can be [combined](https://clerk.com/docs/references/nextjs/clerk-middleware#combine-middleware) with other middlewares like the one provided by `next-intl`. By combining them, the middleware from `@clerk/next` will first ensure protected routes are handled appropriately. Subsequently, the middleware from `next-intl` will run, potentially redirecting or rewriting incoming requests.

```tsx filename="middleware.ts"
import {clerkMiddleware, createRouteMatcher} from '@clerk/nextjs/server';
import createMiddleware from 'next-intl/middleware';
import {locales} from './config';

const intlMiddleware = createMiddleware({
  locales,
  defaultLocale: 'en'
});

const isProtectedRoute = createRouteMatcher(['/:locale/dashboard(.*)']);

export default clerkMiddleware((auth, req) => {
  if (isProtectedRoute(req)) auth().protect();

  return intlMiddleware(req);
});

export const config = {
  // Match only internationalized pathnames
  matcher: ['/', '/(de|en)/:path*']
};
```

(based on `@clerk/nextjs@^5.0.0`)

### Example: Integrating with Supabase Authentication

In order to use Supabase Authentication with `next-intl`, you need to combine the Supabase middleware with the one from `next-intl`.

You can do so by following the [setup guide from Supabase](https://supabase.com/docs/guides/auth/server-side/nextjs?router=app) and adapting the middleware utils to accept a response object that's been created by the `next-intl` middleware instead of creating a new one:

```tsx filename="utils/supabase/middleware.ts"
import {createServerClient} from '@supabase/ssr';
import {NextResponse, type NextRequest} from 'next/server';

export async function updateSession(
  request: NextRequest,
  response: NextResponse
) {
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll();
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({name, value}) =>
            request.cookies.set(name, value)
          );
          cookiesToSet.forEach(({name, value, options}) =>
            response.cookies.set(name, value, options)
          );
        }
      }
    }
  );

  const {
    data: {user}
  } = await supabase.auth.getUser();

  return response;
}
```

Now, we can integrate the Supabase middleware with the one from `next-intl`:

```tsx filename="middleware.ts"
import createMiddleware from 'next-intl/middleware';
import {type NextRequest} from 'next/server';
import {locales, defaultLocale} from '@/app/_config/config';
import {updateSession} from '@/utils/supabase/middleware';

const i18nMiddleware = createMiddleware({
  locales,
  defaultLocale
});

export async function middleware(request: NextRequest) {
  const response = i18nMiddleware(request);

  // A `response` can now be passed here
  return await updateSession(request, response);
}

export const config = {
  matcher: ['/', '/(de|en)/:path*']
};
```

(based on `@supabase/ssr@^0.5.0`)

### Example: Integrating with Auth.js (aka NextAuth.js) [#example-auth-js]

The Next.js middleware of [Auth.js](https://authjs.dev/) requires an integration with their control flow to be compatible with other middlewares. The [success callback](https://next-auth.js.org/configuration/nextjs#wrap-middleware) can be used to run the `next-intl` middleware on authorized pages. However, public pages need to be treated separately.

For pathnames specified in [the `pages` object](https://next-auth.js.org/configuration/nextjs#pages) (e.g. `signIn`), Auth.js will skip the entire middleware and not run the success callback. Therefore, we have to detect these pages before running the Auth.js middleware and only run the `next-intl` middleware in this case.

```tsx filename="middleware.ts"
import {withAuth} from 'next-auth/middleware';
import createMiddleware from 'next-intl/middleware';
import {NextRequest} from 'next/server';
import {locales} from './config';

const publicPages = ['/', '/login'];

const intlMiddleware = createMiddleware({
  locales,
  defaultLocale: 'en'
});

const authMiddleware = withAuth(
  // Note that this callback is only invoked if
  // the `authorized` callback has returned `true`
  // and not for pages listed in `pages`.
  function onSuccess(req) {
    return intlMiddleware(req);
  },
  {
    callbacks: {
      authorized: ({token}) => token != null
    },
    pages: {
      signIn: '/login'
    }
  }
);

export default function middleware(req: NextRequest) {
  const publicPathnameRegex = RegExp(
    `^(/(${locales.join('|')}))?(${publicPages
      .flatMap((p) => (p === '/' ? ['', '/'] : p))
      .join('|')})/?$`,
    'i'
  );
  const isPublicPage = publicPathnameRegex.test(req.nextUrl.pathname);

  if (isPublicPage) {
    return intlMiddleware(req);
  } else {
    return (authMiddleware as any)(req);
  }
}

export const config = {
  matcher: ['/((?!api|_next|.*\\..*).*)']
};
```

(based on `next-auth@^4.0.0`)

<Callout>

Have a look at the [`next-intl` with NextAuth.js example](/examples#app-router-next-auth) to explore a working setup.

</Callout>

## Usage without middleware (static export)

If you're using the [static export](https://nextjs.org/docs/app/building-your-application/deploying/static-exports) feature from Next.js (`output: 'export'`), the middleware will not run. You can use [prefix-based routing](/docs/routing#locale-prefix) nontheless to internationalize your app, but a few tradeoffs apply.

**Static export limitations:**

1. There's no default locale that can be used without a prefix (same as [`localePrefix: 'always'`](/docs/routing#locale-prefix-always))
2. The locale can't be negotiated at runtime (same as [`localeDetection: false`](#locale-detection-false))
3. You can't use [pathname localization](/docs/routing#pathnames)
4. This requires [static rendering](/docs/getting-started/app-router/with-i18n-routing#static-rendering)
5. You need to add a redirect for the root of the app

```tsx filename="app/page.tsx"
import {redirect} from 'next/navigation';

// Redirect the user to the default locale when `/` is requested
export default function RootPage() {
  redirect('/en');
}
```

Note that other [limitations as documented by Next.js](https://nextjs.org/docs/app/building-your-application/deploying/static-exports#unsupported-features) will apply too.

## Troubleshooting

### "Unable to find `next-intl` locale because the middleware didn't run on this request." [#unable-to-find-locale]

This can happen either because:

1. You're using a setup _with_ [i18n routing](/docs/getting-started/app-router) but the middleware is not set up.
2. You're using a setup _without_ [i18n routing](/docs/getting-started/app-router) but are reading the `locale` param passed to the function within `getRequestConfig`.
3. The middleware is set up in the wrong file (e.g. you're using the `src` folder, but `middleware.ts` was added in the root folder).
4. The middleware matcher didn't match a request, but you're using APIs from `next-intl` in a component.
5. You're attempting to implement static rendering via [`force-static`](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic).

To recover from this error, please make sure that:

1. You're consistently using a setup with or without [i18n routing](/docs/getting-started/app-router) (i.e. with or without the [routing APIs](/docs/routing)).
2. If you're using a setup _with_ i18n routing:
   1. You're using APIs from `next-intl` (including [the navigation APIs](/docs/routing/navigation)) exclusively within the `[locale]` segment.
   2. Your [middleware matcher](#matcher-config) matches all routes of your application, including dynamic segments with potentially unexpected characters like dots (e.g. `/users/jane.doe`).
   3. If you're using [`localePrefix: 'as-needed'`](/docs/routing#locale-prefix-as-needed), the `locale` segment effectively acts like a catch-all for all unknown routes. You should make sure that the `locale` is [validated](/docs/usage/configuration#i18nts) before it's used by any APIs from `next-intl`.
   4. To implement static rendering, make sure to [provide a static locale](/docs/getting-started/app-router/with-i18n-routing#static-rendering) to `next-intl` instead of using `force-static`.
3. If you're using using a setup _without_ i18n routing:
   1. You don't read the `locale` param in `getRequestConfig` but instead return it.

Note that `next-intl` will invoke the `notFound()` function to abort the render if the locale can't be found. You should consider adding [a `not-found` page](/docs/environments/error-files#not-foundjs) due to this.
